{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"right\" style=\"max-width: 200px; height: auto\" src=\"cfds_logo.png\">\n",
    "\n",
    "###  Lab 10 - \"Long Short-Term Memory (LSTM) Neural Networks\"\n",
    "\n",
    "Chartered Financial Data Scientist (CFDS), Spring Term 2020"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this lab, we will learn how to apply another type of deep learning technique referred to as **Long Short-Term Memory (LSTM)** neural networks. Unlike standard feedforward neural networks, LSTMs encompass feedback connections that make it a \"general-purpose computer\". LSTMs are designed to process not only a single data point (such as images), but also entire sequences of data, e.g., such as speech, video, or financial time series.\n",
    "\n",
    "\n",
    "We will again use the functionality of the **PyTorch** library to implement and train an LSTM based neural network. The network will be trained on the historic daily (in-sample) returns of an exemplary financial stock. Once the network is trained, we will use the learned model to predict future (out-of-sample) returns. Finally, we will convert the predictions into tradable signals and the backtest the signals accordingly. \n",
    "\n",
    "The figure below illustrates a high-level view on the machine learning process we aim to establish in this lab."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 700px\" src=\"process.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As always, pls. don't hesitate to ask all your questions either during the lab or send us an email via marco.schreyer@fds.ai or damian.borth@fds.ai."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Lab Objectives:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "After today's lab, you should be able to:\n",
    "\n",
    "> 1. Understand the basic concepts, intuitions and major building blocks of **Long Short-Term Memory (LSTM) Neural Networks**.\n",
    "> 2. Know how to **implement and to train an LSTM** to learn a model of financial time-series data.\n",
    "> 3. Understand how to apply such a learned model to **predict future data points of a time-series**.\n",
    "> 4. Know how to **interpret the model's prediction results** and backtest the predictions. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before we start let's watch a motivational video:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import YouTubeVideo\n",
    "# Nvidia GTC 2016: \"The Deep Learning Revolution\" Opening in Keynote\"\n",
    "YouTubeVideo('Dy0hJWltsyE', width=800, height=400)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup of the Jupyter Notebook Environment"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Suppress potential warnings:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Similar to the previous labs, we need to import a couple of Python libraries that allow for data analysis and data visualization. We will mostly use the `PyTorch`, `Numpy`, `Sklearn`, `Matplotlib`, `Seaborn`, `BT` and a few utility libraries throughout the lab:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# import python data science and utility libraries\n",
    "import os, io, urllib, itertools\n",
    "import datetime as dt\n",
    "import pandas as pd\n",
    "import pandas_datareader as dr\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Install the Python `BT` backtesting library:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install bt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Import the backtesting library:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import bt as bt # library to backtest trading signals"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Import the Python machine / deep learning libraries:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# pytorch libraries\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "from torch.utils import data\n",
    "from torch.utils.data import dataloader"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Import Python plotting libraries and set general plotting parameters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "plt.style.use('seaborn')\n",
    "plt.rcParams['figure.figsize'] = [10, 5]\n",
    "plt.rcParams['figure.dpi']= 150"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Enable notebook matplotlib inline plotting:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create notebook folder structure to store the data as well as the trained neural network models:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if not os.path.exists('./data'): os.makedirs('./data')  # create data directory\n",
    "if not os.path.exists('./models'): os.makedirs('./models')  # create trained models directory"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Set random seed value to obtain reproducable results:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# init deterministic seed\n",
    "seed_value = 1234\n",
    "np.random.seed(seed_value) # set numpy seed\n",
    "torch.manual_seed(seed_value); # set pytorch seed CPU"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Enable GPU computing by setting the `device` flag and init a `CUDA` seed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# set cpu or gpu enabled device\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu').type\n",
    "\n",
    "# init deterministic GPU seed\n",
    "torch.cuda.manual_seed(seed_value)\n",
    "\n",
    "# log type of device enabled\n",
    "now = dt.datetime.utcnow().strftime(\"%Y%m%d-%H:%M:%S\")\n",
    "print('[LOG {}] notebook with \\'{}\\' computation enabled'.format(str(now), str(device)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Dataset Download and Data Assessment"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section of the lab notebook we will download and access historic daily stock market data ranging from **01/01/2000** to **31/12/2017** of the **\"International Business Machines\" (IBM)** corporation (ticker symbol: \"IBM\"). Thereby, we will utilize the `datareader` of the `Pandas` library that provides the ability to interface the `Yahoo` finance API.\n",
    "\n",
    "To start the data download, let's specify the start and end date of the stock market data download:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "start_date = dt.datetime(2000, 1, 1)\n",
    "end_date = dt.datetime(2017, 12, 31)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Download the daily \"International Business Machines\" (IBM) stock market data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_data = dr.data.DataReader('IBM', data_source='yahoo', start=start_date, end=end_date)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the top 10 records of the retreived IBM stock market data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_data.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's also evaluate the data quality of the download by creating a set of summary statistics of the retrieved data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_data.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visually inspect the daily closing prices of the \"International Business Machines\" (IBM) stock market data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "# plot reconstruction error scatter plot\n",
    "ax.plot(stock_data.index, stock_data['Close'], color='#9b59b6')\n",
    "\n",
    "for tick in ax.get_xticklabels():\n",
    "    tick.set_rotation(45)\n",
    "\n",
    "# set x-axis labels and limits\n",
    "ax.set_xlabel('[time]', fontsize=10)\n",
    "ax.set_xlim([pd.to_datetime('01-01-2000'), pd.to_datetime('31-12-2017')])\n",
    "\n",
    "# set y-axis labels and limits\n",
    "ax.set_ylabel('[stock adj. closing price]', fontsize=10)\n",
    "ax.set_ylim(20, 220)\n",
    "\n",
    "# set plot title\n",
    "plt.title('International Business Machines (IBM) - Daily Historical Stock Closing Prices', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Save the obtained and validated stock market data to the local data directory:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# save retrieved data to local data directory\n",
    "stock_data.to_csv('data/ibm_data_2010_2017_daily.csv', sep=';', encoding='utf-8')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Data Pre-Processing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, we will obtain daily returns of the retrieved daily closing prices. Also, we will convert the time-series of daily returns into a set of sequences $s$ of $n$ time steps respectively. The created sequences will then be used to learn a model using an Long Short-Term Memory neural network."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.1 Weekend and Holiday Padding"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's always forward propagate the last valid available price information observation to the next available valid price information using the Panda's `reindex()` function. This in order to also obtain market price information of weekend's and holidays: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# fill weekends and holidays\n",
    "stock_data = stock_data.reindex(index=pd.date_range(stock_data.index.min(), stock_data.index.max()), method='ffill')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the padded stock market data of the \"International Business Machines\" (IBM) stock:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_data.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the number of records obtained after the data padding:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_data.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.2 Daily Return Calculation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine the daily returns of the \"International Business Machines\" (IBM) daily closing prices using the Panda's `pct_change()` function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_data['RETURN'] = stock_data['Close'].pct_change()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the daily returns of the closing prices:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_data['RETURN']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visually inspect the obtained daily returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "# plot reconstruction error scatter plot\n",
    "ax.plot(stock_data.index, stock_data['RETURN'], color='#9b59b6')\n",
    "\n",
    "for tick in ax.get_xticklabels():\n",
    "    tick.set_rotation(45)\n",
    "\n",
    "# set axis labels and limits\n",
    "ax.set_xlabel('[time]', fontsize=10)\n",
    "ax.set_xlim([pd.to_datetime('01-01-2000'), pd.to_datetime('31-12-2017')])\n",
    "ax.set_ylabel('[daily stock returns]', fontsize=10)\n",
    "\n",
    "# set plot title\n",
    "plt.title('International Business Machines (IBM) - Daily Historical Stock Closing Prices', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.3 Conduct Train-Test Split for Neural Network Training"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To understand and evaluate the performance of any trained **supervised machine learning** model, it is good practice, to divide the dataset into a **training set** or **\"in-sample\"** data (the fraction of data records solely used for training purposes) and a **evaluation set** or **\"out-of-sample\"** data (the fraction of data records solely used for evaluation purposes). Pls. note, the **evaluation set** will never be shown to the model as part of the training process."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 500px\" src=\"traintestsplit.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We set the split fraction of training sequences to **90%** of the total number of obtained sequences:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "split_fraction = 0.9\n",
    "split_row = int(stock_data.shape[0] * split_fraction)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Split obtained returns into training (\"in-sample\") returns $r^{i}_{train}$ and validation (\"out-of-sample\") returns $r^{i}_{valid}$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_stock_data = stock_data.iloc[:split_row]\n",
    "valid_stock_data = stock_data.iloc[split_row:]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visually inspect the obtained train and validation stock returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "# plot daily stock returns\n",
    "ax.plot(stock_data.index[:split_row,], train_stock_data['RETURN'], c='C0', label='train')\n",
    "ax.plot(stock_data.index[split_row:,], valid_stock_data['RETURN'], c='C1', label='valid')\n",
    "\n",
    "# rotate x-labels 45 degree angle\n",
    "for tick in ax.get_xticklabels():\n",
    "    tick.set_rotation(45)\n",
    "\n",
    "# set axis labels and limits\n",
    "ax.set_xlabel('[time]', fontsize=10)\n",
    "ax.set_xlim([pd.to_datetime('01-01-2000'), pd.to_datetime('31-12-2017')])\n",
    "ax.set_ylabel('[daily stock returns]', fontsize=10)\n",
    "\n",
    "# set plot legend\n",
    "plt.legend(loc=\"lower right\", numpoints=1, fancybox=True)\n",
    "\n",
    "# set plot title\n",
    "plt.title('International Business Machines (IBM) - Daily Historical Stock Returns', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine count (shape) of daily return train sequences $r^{i}_{train}$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_stock_data.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine count (shape) of daily return train sequences $r^{i}_{valid}$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "valid_stock_data.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.4 Transform Time-Series Into Sequences"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the following, we determine the number of return time-steps $n$ each individual sequence $s^{i}$ should be comprised of. Each sequence is thereby determined by the number of predictor (return) time-steps $t$ and the prediction (return) horizon $h = t+1$."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 500px\" src=\"timesteps.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, we will set the number of predictor (return) time-steps to $t$=4. This indicates that the input sequence of each sample is a vector of 4 sequential daily stock returns (pls. note, the choice of $t$=4 is arbitrary and should be selected through experimentation). Furthermore, we set the predicted return horizon to 1, which specifies that we aim to forecast a single future time-step."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "time_steps = 4 # number of predictor timesteps\n",
    "horizon = 1 # number of timesteps to be predicted\n",
    "sequence_length = time_steps + horizon # determine sequence length"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, we extract the sequences $s^i$ of 5 time-steps. \n",
    "\n",
    "Thereby, we will step-wise iterate (\"rolling window\") over the entire sequence of daily stock returns $r_i$. In each iteration step, we extract an individual sequence of stock returns consisting of $n$ time-steps. The extracted individual sequences of daily closing prices are then collected in a single data frame."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 900px\" src=\"sequences.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine the max number of training (\"in-sample\") sequences:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# determine max train index\n",
    "max_train_index = ((train_stock_data.shape[0] // sequence_length) - 1) * sequence_length"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Extract individual training sequences of length $5$ from the obtained daily returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# iterate over the distinct daily returns of the training dataset\n",
    "for i in range(1, max_train_index):\n",
    "\n",
    "    # case: initial sequence\n",
    "    if i == 1:\n",
    "\n",
    "        # convert to numpy array and collect sequence of timesteps and daily returns\n",
    "        train_stock_sequence_data_date = np.array(train_stock_data.index[i:i + sequence_length].T)\n",
    "        train_stock_sequence_data = np.array(train_stock_data['RETURN'][i:i + sequence_length].T)\n",
    "\n",
    "    # case: non-initial sequence\n",
    "    else:\n",
    "\n",
    "        # convert to numpy array and collect sequence of timesteps and daily returns\n",
    "        train_stock_sequence_data_date = np.vstack((train_stock_sequence_data_date, np.array(train_stock_data.index[i:i + sequence_length].T)))\n",
    "        train_stock_sequence_data = np.vstack((train_stock_sequence_data, np.array(train_stock_data['RETURN'][i:i + sequence_length].T)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine the total number of obtained training sequences:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_stock_sequence_data.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the top five collected sequences of training timesteps:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_stock_sequence_data_date[0:5,]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the top five collected sequences of training returns $s^{i}_{train}=\\{r_{t-n-1}, ..., r_{t-1}, r_{t}\\}$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_stock_sequence_data[0:5,]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine the max number of validation (\"out-of-sample\") sequences:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# determine max valid index\n",
    "max_valid_index = ((valid_stock_data.shape[0] // sequence_length) - 1) * sequence_length"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Extract individual validation sequences of length $5$ from the obtained daily returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# iterate over the distinct daily returns of the validation dataset\n",
    "for i in range(1, max_valid_index):\n",
    "\n",
    "    # case: initial sequence\n",
    "    if i == 1:\n",
    "\n",
    "        # convert to numpy array and collect sequence of timesteps and daily returns\n",
    "        valid_stock_sequence_data_date = np.array(valid_stock_data.index[i:i + sequence_length].T)\n",
    "        valid_stock_sequence_data = np.array(valid_stock_data['RETURN'][i:i + sequence_length].T)\n",
    "\n",
    "\n",
    "    # case: non-initial sequence\n",
    "    else:\n",
    "\n",
    "        # convert to numpy array and collect sequence of timesteps and daily returns\n",
    "        valid_stock_sequence_data_date = np.vstack((valid_stock_sequence_data_date, np.array(valid_stock_data.index[i:i + sequence_length].T)))\n",
    "        valid_stock_sequence_data = np.vstack((valid_stock_sequence_data, np.array(valid_stock_data['RETURN'][i:i + sequence_length].T)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine the total number of obtained validation sequences:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "valid_stock_sequence_data.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the top five collected sequences of validation timesteps:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "valid_stock_sequence_data_date[0:5,]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the top five collected sequences of validation returns $s^{i}_{valid}=\\{r_{t-n-1}, ..., r_{t-1}, r_{t}\\}$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "valid_stock_sequence_data[0:5,]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.4 Conduct Input-Target Split for Neural Network Training"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before we continue the date pre-processing, let's briefly revisit how RNN's or, more specifically, LSTM based NN's can be trained to predict the next element of an input sequence. The cartoon below is derived from the \"Next Word Predictor\" Example that we also discussed in the course. For each **input return** $r_{i}$ of the input return training sequence $s^i$ the LSTM is supposed to learn to **predict the return** of the next time-step $\\hat{r}_{i+1}$. In order to make such a future return $\\hat{r}_{i+1}$ prediction the LSTM uses it's learned hidden state information $h_{i}$ as well as the current return $r_{i}$ as an input. \n",
    "\n",
    "For each time-step the predicted return $\\hat{r}_{i+1}$ is then compared to the **target return** $r_{i+1}$. The discrepancy between both is collected as a loss $\\mathcal{L}$ for the distinct timesteps. The accumulation of the individual time-step losses is accumulated as the total loss of a sequence $\\mathcal{L}_{All}$."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 600px\" src=\"training.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seperate each training sequence $s^{i}$ into time-steps of input returns denoted by $s^{i}_{train, input}=\\{r_{t-n-1}, ..., r_{t-1}, r_{t}\\}$ and the time-step of the to be predicted target return denoted by $s^{i}_{train, target}=r_{t+1}$."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 700px\" src=\"sequencesplit.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In addition, we convert both the input returns as well as the target returns to PyTorch tensors:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_sequences_input = torch.from_numpy(train_stock_sequence_data[:, :-1]).float()\n",
    "train_sequences_target = torch.from_numpy(train_stock_sequence_data[:, 1:]).float()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seperate each validation sequence $s^{i}$ into time-steps of input returns denoted by $s^{i}_{valid, input}=\\{r_{t-n-1}, ..., r_{t-1}, r_{t}\\}$ and the time-step of the to be predicted target return denoted by $s^{i}_{valid, target}=r_{t+1}$. In addition, we convert both the input returns as well as the target returns to PyTorch tensors:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "valid_sequences_input = torch.from_numpy(valid_stock_sequence_data[:, :-1]).float()\n",
    "valid_sequences_target = torch.from_numpy(valid_stock_sequence_data[:, 1:]).float()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To train an LSTM neural network, we tailor the dataset class provided by the PyTorch library. We overwrite the individual functions of the dataset class. So that our dataset will supply the neural network with the individual training sequences $s^{i}_{train, input}$ and corresponding targets $s^{i}_{train, target}$ throughout the training process:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# define daily returns dataset\n",
    "class DailyReturnsDataset(data.Dataset):\n",
    "\n",
    "    # define the class constructor\n",
    "    def __init__(self, sequences, targets):\n",
    "\n",
    "        # init sequences and corresponding targets\n",
    "        self.sequences = sequences\n",
    "        self.targets = targets\n",
    "\n",
    "    # define the length method \n",
    "    def __len__(self):\n",
    "\n",
    "        # returns the number of samples\n",
    "        return len(self.targets)\n",
    "\n",
    "    # define the get item method\n",
    "    def __getitem__(self, index):\n",
    "\n",
    "        # determine single sequence and corresponding target\n",
    "        sequence = self.sequences[index, :]\n",
    "        target = self.targets[index, :]\n",
    "\n",
    "        # return sequences and target\n",
    "        return sequence, target"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once we have specified the daily returns dataset class we instantiate it using the new daily closing dataset using the prepared training input sequences $s^{i}_{train, input}$ and corresponding targets $s^{i}_{train, target}$: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_dataset = DailyReturnsDataset(train_sequences_input, train_sequences_target)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's see how it works by getting the 42th sequence and its corresponding targets:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_dataset.__getitem__(42)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Neural Network Implementation and Loss Function"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, we will implement the LSTM architecture of the to be learned time series model. Furthermore, we will specify the loss-function, learning-rate and optimization technique used in the network training."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 3.1. Implementation of the LSTM Architecture"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, we will implement the architecture of the LSTM neural network utilized to predict future returns of financial time series data, e.g. as in this example, the future returns of a given stock. The neural network, which we name **'LSTMNet'** consists in total of three layers. The first two layers correspond to LSTM cells, while the third layer corresponds to a fully-connected linear layer."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 400px\" src=\"lstmnet.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The general LSTM cell structure as well as the formal definition of its individual gate functions are shown in the following (not considering the bias of each layer for simplicity):"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img align=\"center\" style=\"max-width: 700px\" src=\"lstmcell.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(Source: https://pytorch.org/docs/stable/nn.html)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Each LSTM layer consits of a LSTM cell exhibiting a hidden state of 51 dimensions. The third linear squeezes the 51 hidden state dimensions of the second LSTM cell into a single output dimension. The single output signal of the linear layer refers to the return of the next time-step predicted by the neural network. Please note, that the choice of the implemented architecture and network hyperparameters is arbitrary and should in a real-world scenario be evaluated and selected thoroughly through experimentation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# implement the LSTMNet network architecture\n",
    "class LSTMNet(nn.Module):\n",
    "\n",
    "    # define class constructor\n",
    "    def __init__(self):\n",
    "\n",
    "        super(LSTMNet, self).__init__()\n",
    "\n",
    "        # define lstm nn architecture\n",
    "        self.lstm1 = nn.LSTMCell(1, 51)  # first lstm layer\n",
    "        self.lstm2 = nn.LSTMCell(51, 51)  # second lstm layer\n",
    "        self.linear = nn.Linear(51, 1)  # final linear layer\n",
    "\n",
    "    # define network forward pass\n",
    "    def forward(self, input):\n",
    "        \n",
    "        # init predictions\n",
    "        predictions = []\n",
    "\n",
    "        # init the lstm hidden states\n",
    "        h_t1 = torch.zeros(input.size(0), 51, dtype=torch.float).to(device)\n",
    "        h_t2 = torch.zeros(input.size(0), 51, dtype=torch.float).to(device)\n",
    "\n",
    "        # init the lstm cell states\n",
    "        c_t1 = torch.zeros(input.size(0), 51, dtype=torch.float).to(device)\n",
    "        c_t2 = torch.zeros(input.size(0), 51, dtype=torch.float).to(device)\n",
    "        \n",
    "        # iterate over distinct time steps\n",
    "        for i, input_t in enumerate(input.chunk(input.size(1), dim=1)):\n",
    "\n",
    "            # propagate through time step data\n",
    "            h_t1, c_t1 = self.lstm1(input_t, (h_t1, c_t1))\n",
    "            h_t2, c_t2 = self.lstm2(h_t1, (h_t2, c_t2))\n",
    "            prediction = self.linear(h_t2)\n",
    "            \n",
    "            # collect predictions\n",
    "            predictions += [prediction]\n",
    "\n",
    "        # stack predictions\n",
    "        predictions = torch.stack(predictions, 1).squeeze(2)\n",
    "\n",
    "        # return predictions\n",
    "        return predictions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, that we have implemented our first LSTM neural network we are ready to instantiate a model to be trained:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lstm_model = LSTMNet().to(device)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once the model is initialized, we can visualize the model structure and review the implemented network architecture by execution of the following cell:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# print the initialized architectures\n",
    "print('[LOG] LSTMNet architecture:\\n\\n{}\\n'.format(lstm_model))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looks like intended? Great! Finally, let's have a look into the number of model parameters that we aim to train in the next steps of the notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# init the number of model parameters\n",
    "num_params = 0\n",
    "\n",
    "# iterate over the distinct parameters\n",
    "for param in lstm_model.parameters():\n",
    "\n",
    "    # collect number of parameters\n",
    "    num_params += param.numel()\n",
    "    \n",
    "# print the number of model paramters\n",
    "print('[LOG] Number of to be trained LSTMNet model parameters: {}.'.format(num_params))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ok, our \"simple\" `LSTMNet` model already encompasses an impressive number **32'284 model parameters** to be trained."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 3.2. Definition of the Training Loss Function and Learning Rate"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We are now good to train the network. However, prior to starting the training, we need to define an appropriate loss function. Remember, we aim to train our model to learn a set of model parameters $\\theta$ that minimize the prediction error of the true return $r_{t+1}$ and the by the model predicted return $\\hat{r}_{t+1}$ at a given time-step $t+1$ of sequence $s^{i}$. In other words, for a given sequence of historic returns we aim to learn a function $f_\\theta$ that is capable to predicts the return of the next timestep as faithfully as possible, as expressed by:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center> $\\hat{r}_{t+1} = f_\\theta(r_{t}, r_{t-1}, ..., r_{t-n})$. </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Thereby, the training objective is to learn a set of optimal model parameters $\\theta^*$ that optimize $\\min_{\\theta} \\|r_{t+1} - f_\\theta(r_{t}, r_{t-1}, ..., r_{t-n})\\|$ over all time-steps $t$ contained in the set of training sequences $s_{train}$. To achieve this optimization objective, one typically minimizes a loss function $\\mathcal{L_{\\theta}}$ while training the neural network. In this lab we use the **'Mean Squared Error (MSE)'** loss, as denoted by:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center> $\\mathcal{L}^{MSE}_{\\theta} (r_{t+1}, \\hat{r}_{t+1}) = \\frac{1}{N} \\sum_{i=1}^N \\| r_{t+1} - \\hat{r}_{t+1}\\|^{2}$, </center>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "loss_function = nn.MSELoss().to(device)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Throughout the training process, the PyTorch library will automatically calculate the loss magnitude, compute the gradient, and update the parameters $\\theta$ of the LSTM neural network. We will use the **\"Adaptive Moment Estimation Optimization\" (ADAM)** technique to optimize the network parameters. Furthermore, we specify a constant learning rate of $l = 1e-06$. For each training step, the optimizer will update the model parameters $\\theta$ values according to the degree of prediction error (the MSE loss)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learning_rate = 1e-06 # set constant learning rate\n",
    "optimizer = optim.Adam(lstm_model.parameters(), lr=learning_rate) # define optimization technique"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have successfully implemented and defined the three ANN building blocks let's take some time to review the `LSTMNet` model definition as well as the `MSE loss` function. Please, read the above code and comments carefully and don't hesitate to let us know any questions you might have."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Training the Neural Network Model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, we will train the LSTM neural network model (as implemented in the section above) using the prepared dataset of daily return sequences. Therefore, we will have a detailed look into the distinct training steps and monitor the training progress."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 4.1. Preparing the Network Training"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's now start to learn a model by training the NN for **5 epochs** in mini-batches of the size of **128  sequences** per batch. This implies that the whole dataset will be fed to the network **5 times** in chunks of 128 sequences yielding to **32 mini-batches** (4'068 training sequences / 128 sequences per mini-batch) per epoch:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# specify the training parameters\n",
    "num_epochs = 200 # number of training epochs\n",
    "mini_batch_size = 128 # size of the mini-batches"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Furthermore, lets specify and instantiate a corresponding PyTorch data loader that feeds the image tensors to our neural network:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dl = dataloader.DataLoader(train_dataset, batch_size=mini_batch_size, shuffle=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 4.2. Running the Network Training"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, we start training the model. The training procedure of each mini-batch is performed as follows: \n",
    "\n",
    ">1. do a forward pass through the LSTMNet network, \n",
    ">2. compute the mean-squared prediction error $\\mathcal{L}^{MSE}_{\\theta} (r_{t+1}, \\hat{r}_{t+1}) = \\frac{1}{N} \\sum_{i=1}^N \\| r_{t+1} - \\hat{r}_{t+1}\\|^{2}$, \n",
    ">3. do a backward pass through the LSTMNet network, and \n",
    ">4. update the parameters of the network $f_\\theta(\\cdot)$.\n",
    "\n",
    "To ensure learning while training the LSTM model we will monitor whether the loss decreases with progressing training. Therefore, we obtain and evaluate the mean prediction performance over all mini-batches in each training epoch. Based on this evaluation we can conclude on the training progress and whether the loss is converging (indicating that the model might not improve any further).\n",
    "\n",
    "The following elements of the network training code below should be given particular attention:\n",
    " \n",
    ">- `loss.backward()` computes the gradients based on the magnitude of the reconstruction loss,\n",
    ">- `optimizer.step()` updates the network parameters based on the gradient."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# init collection of training epoch losses\n",
    "train_epoch_losses = []\n",
    "\n",
    "# set the model in training mode\n",
    "lstm_model.train()\n",
    "\n",
    "# init the best loss\n",
    "best_loss = 100.00\n",
    "\n",
    "# iterate over epochs\n",
    "for epoch in range(0, num_epochs):\n",
    "\n",
    "    # init collection of mini-batch losses\n",
    "    train_mini_batch_losses = []\n",
    "            \n",
    "    # iterate over mini-batches\n",
    "    for sequence_batch, target_batch in dl:\n",
    "        \n",
    "        # push mini-batch data to computation device\n",
    "        sequence_batch = sequence_batch.to(device)\n",
    "        target_batch = target_batch.to(device)\n",
    "\n",
    "        # predict sequence output\n",
    "        prediction_batch = lstm_model(sequence_batch)\n",
    "\n",
    "        # calculate batch loss\n",
    "        batch_loss = loss_function(prediction_batch, target_batch)\n",
    "\n",
    "        # run backward gradient calculation\n",
    "        batch_loss.backward()\n",
    "\n",
    "        # update network parameters\n",
    "        optimizer.step()\n",
    "        \n",
    "        # collect mini-batch loss\n",
    "        train_mini_batch_losses.append(batch_loss.data.item())\n",
    "            \n",
    "    # determine mean min-batch loss of epoch\n",
    "    train_epoch_loss = np.mean(train_mini_batch_losses)\n",
    "    \n",
    "    # print epoch loss\n",
    "    now = dt.datetime.utcnow().strftime(\"%Y%m%d-%H:%M:%S\")\n",
    "    print('[LOG {}] epoch: {} train-loss: {}'.format(str(now), str(epoch), str(train_epoch_loss)))\n",
    "    \n",
    "    # determine mean min-batch loss of epoch\n",
    "    train_epoch_losses.append(train_epoch_loss)\n",
    "        \n",
    "    # print epoch and save models\n",
    "    if epoch % 10 == 0 and epoch > 0:\n",
    "        \n",
    "        # case: new best model trained\n",
    "        if train_epoch_loss < best_loss:\n",
    "                        \n",
    "            # store new best model\n",
    "            model_name = 'best_lstm_model_{}.pth'.format(str(epoch))\n",
    "            torch.save(lstm_model.state_dict(), os.path.join(\"./models\", model_name))\n",
    "            \n",
    "            # update best loss\n",
    "            best_loss = train_epoch_loss\n",
    "            \n",
    "            # print epoch loss\n",
    "            now = dt.datetime.utcnow().strftime(\"%Y%m%d-%H:%M:%S\")\n",
    "            print('[LOG {}] epoch: {} new best train-loss: {} found'.format(str(now), str(epoch), str(train_epoch_loss)))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Upon successful training let's visualize and inspect the training loss per epoch:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# prepare plot\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "# add grid\n",
    "ax.grid(linestyle='dotted')\n",
    "\n",
    "# plot the training epochs vs. the epochs' prediction error\n",
    "ax.plot(np.array(range(1, len(train_epoch_losses)+1)), train_epoch_losses, label='epoch loss (blue)')\n",
    "\n",
    "# add axis legends\n",
    "ax.set_xlabel(\"[training epoch $e_i$]\", fontsize=10)\n",
    "ax.set_ylabel(\"[Prediction Error $\\mathcal{L}^{MSE}$]\", fontsize=10)\n",
    "\n",
    "# set plot legend\n",
    "plt.legend(loc=\"upper right\", numpoints=1, fancybox=True)\n",
    "\n",
    "# add plot title\n",
    "plt.title('Training Epochs $e_i$ vs. Prediction Error $L^{MSE}$', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ok, fantastic. The training error is nicely going down. We could train the network a couple more epochs until the error converges. But let's stay with the 200 training epochs for now and continue with evaluating our trained model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Evaluation of the Trained Neural Network Model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, we will conduct a visual comparison of the predicted daily returns to the actual ('true') daily returns. The comparison will encompass the daily returns of the in-sample time period as well as the returns of the out-of-sample time period."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.1. In-Sample Evaluation of the Trained Neural Network Model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before starting our evaluation, let's load the best performing model or an already pre-trained model (as done below). Remember, that we stored a snapshot of the model after each training epoch to our local model directory. We will now load one of the (hopefully well-performing) snapshots saved."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# init the pre-trained model architecture\n",
    "lstm_model_pretrained = LSTMNet().to(device)\n",
    "\n",
    "# restore pretrained model checkpoint\n",
    "github_model_path = 'https://raw.githubusercontent.com/financial-data-science/CFDS/master/lab_10/models/'\n",
    "lstm_model_name_pretrained = 'best_lstm_model_30000.pth'\n",
    "\n",
    "# Read stored model from the remote location\n",
    "lstm_bytes = urllib.request.urlopen(os.path.join(github_model_path, lstm_model_name_pretrained))\n",
    "\n",
    "# Load tensor from io.BytesIO object\n",
    "lstm_buffer = io.BytesIO(lstm_bytes.read())\n",
    "\n",
    "# load trained models\n",
    "lstm_model_pretrained.load_state_dict(torch.load(lstm_buffer, map_location='cpu'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's inspect if the model was loaded successfully: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# set model in evaluation mode\n",
    "lstm_model_pretrained.eval()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use the pre-trained model to determine the daily return predictions of the **in-sample** sequence population:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# don't calculate gradients\n",
    "with torch.no_grad():\n",
    "\n",
    "    # predict sequence output\n",
    "    train_predictions = lstm_model_pretrained(train_sequences_input.to(device))\n",
    "\n",
    "    # collect prediction batch results\n",
    "    train_predictions_list = train_predictions.cpu().detach().numpy()[:, -1].tolist()\n",
    "\n",
    "    # collect target batch results\n",
    "    train_targets_list = train_sequences_target.numpy()[:, -1].tolist()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Plot the pre-trained `LSTMNet` daily **in-sample** predictions vs. the target (\"ground-truth\") daily returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# plot the prediction results\n",
    "plt.style.use('seaborn')\n",
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "ax.plot(train_stock_sequence_data_date[:, -1], train_targets_list, color='C1', label='groundtruth (green)')\n",
    "ax.plot(train_stock_sequence_data_date[:, -1], train_predictions_list, color='C0', label='predictions (blue)')\n",
    "\n",
    "# set y-axis limits\n",
    "ax.set_xlim(train_stock_sequence_data_date[:, -1].min(), train_stock_sequence_data_date[:, -1].max())\n",
    "\n",
    "# set plot legend\n",
    "plt.legend(loc=\"lower right\", numpoints=1, fancybox=True)\n",
    "\n",
    "# set plot title\n",
    "plt.title('LSTM NN In-Sample Prediction vs. Ground-Truth Market Prices', fontsize=10)\n",
    "\n",
    "# set axis labels\n",
    "plt.xlabel('[time]', fontsize=8)\n",
    "plt.ylabel('[market price]', fontsize=8)\n",
    "\n",
    "# set axis ticks fontsize\n",
    "plt.xticks(fontsize=8)\n",
    "plt.yticks(fontsize=8);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 5.2. Out-of-Sample Evaluation of the Trained Neural Network Model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use the pre-trained model to determine the daily return predictions of the **out-of-sample** sequence population:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# don't calculate gradients\n",
    "with torch.no_grad():\n",
    "\n",
    "    # predict sequence output\n",
    "    valid_predictions = lstm_model_pretrained(valid_sequences_input.to(device))\n",
    "\n",
    "    # collect prediction batch results\n",
    "    valid_predictions_list = valid_predictions.cpu().detach().numpy()[:, -1].tolist()\n",
    "\n",
    "    # collect target batch results\n",
    "    valid_targets_list = valid_sequences_target.numpy()[:, -1].tolist()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Plot the pre-trained `LSTMNet` daily **out-of-sample** predictions vs. the target (\"ground-truth\") daily returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# plot the prediction results\n",
    "plt.style.use('seaborn')\n",
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "ax.plot(valid_stock_sequence_data_date[:, -1], valid_targets_list, color='C1', label='groundtruth (green)')\n",
    "ax.plot(valid_stock_sequence_data_date[:, -1], valid_predictions_list, color='C0', label='predictions (blue)')\n",
    "\n",
    "# set y-axis limits\n",
    "ax.set_xlim(valid_stock_sequence_data_date[:, -1].min(), valid_stock_sequence_data_date[:, -1].max())\n",
    "\n",
    "# set plot legend\n",
    "plt.legend(loc=\"lower right\", numpoints=1, fancybox=True)\n",
    "\n",
    "# set plot title\n",
    "plt.title('LSTM NN Out-of-Sample Prediction vs. Ground-Truth Market Prices', fontsize=10)\n",
    "\n",
    "# set axis labels\n",
    "plt.xlabel('[time]', fontsize=8)\n",
    "plt.ylabel('[market price]', fontsize=8)\n",
    "\n",
    "# set axis ticks fontsize\n",
    "plt.xticks(fontsize=8)\n",
    "plt.yticks(fontsize=8);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Backtest of the Trained Neural Network Model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, we will backtest the python `bt` library. Python `bt` is a flexible, backtest framework that can be used to test quantitative trading strategies. In general, backtesting is the process of testing a strategy over a given data set (more details about the `bt` library can be found via: https://pmorissette.github.io/bt/). \n",
    "\n",
    "In order to test the predictions derived from the LSTM model we will view its predictions $\\hat{r}_{i+1}$ as trade signals $\\phi$. Thereby, we will interpret any positive future return prediction $r_{t+1} > 0.0$ of a sequence $s^i$ as a \"long\" (buy) signal. Likewise, we will interpret any negative future return prediction $r_{t+1} < 0.0$ of a sequence $s$ as a \"short\" (sell) signal."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 6.1. LSTM Trading Signal Preparation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's start by converting the out-of-sample model predictions into a trading signal, as described above. Therefore, we first convert the obtained predictions into a data frame that contains (1) the **date of the predicted returns** as well as (2) the **predicted returns $r_{t+1}$** itself:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_data = pd.DataFrame(valid_predictions_list, columns=['PREDICTIONS'], index=valid_stock_sequence_data_date[:, -1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Furthermore, let's briefly ensure the successful conversion by inspecting the top 10 rows of the created data frame:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_data.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, let's derive a trading signal from the converted predictions. As already described, we will generate the trading signal $\\phi$ according to the following function:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "$\n",
    "\\\\\n",
    "\\phi(\\hat{r}_{t+1})=\n",
    "\\begin{cases}\n",
    "1.0, & for & \\hat{r}_{t+1} > 0.0\\\\\n",
    "-1.0, & for & \\hat{r}_{t+1} < 0.0\\\\\n",
    "\\end{cases}\n",
    "$\n",
    "</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "where $\\hat{r}_{t+1}$ denotes a by the model predicted future return at time $t+1$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_data['SIGNAL'] = np.where(signal_data['PREDICTIONS'] > 0.0, 1.0, -1.0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's inspect the top 10 rows of the prepared trading signals:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_data.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's now offset the prepared trading signal by a single day $t-1$. Thereby, we rebalance our stock positions one day prior and according to the closing price predicted by the `LSTMNet` model. As a result we will be able to anticipate the model's future closing price prediction for a particular day $t$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_data = signal_data.set_index(signal_data['SIGNAL'].index - pd.DateOffset(1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's inspect the top 10 rows of the prepared and offset trading signals:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_data.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visualize the predicted and prepared trading signals of the `LSTMNet` model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "ax.plot(signal_data['SIGNAL'], lw=1.0, color='C3', label='LSTM trade signals')\n",
    "    \n",
    "# set axis ranges\n",
    "ax.set_xlim([signal_data.index[0], signal_data.index[-1]])\n",
    "ax.set_ylim([-1.1, 1.1])\n",
    "\n",
    "# set axis labels\n",
    "ax.set_xlabel('[time]', fontsize=10)\n",
    "ax.set_ylabel('[lstm tade signal]', fontsize=10)\n",
    "\n",
    "# rotate x-axis ticks\n",
    "for tick in ax.get_xticklabels():\n",
    "    tick.set_rotation(45)\n",
    "\n",
    "# set plot title\n",
    "ax.set_title('International Business Machines Corporation (IBM) - LSTM Trading Signals', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determine the number of trade signal changes (trades to be executed) within the out-of-sample timeframe **03/2016** until **12/2017**, resulting in a total in-sample timeframe of approx. **21 months** (9 + 12):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# determine number of signal changes\n",
    "len(list(itertools.groupby(signal_data['SIGNAL'], lambda x: x > 0)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "On average around **7** signal changes (trades) per month (148 signal changes / 21 months) within the out-of-sample time period."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 6.2. Stock Market Data Preperation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, let's prepare the daily closing prices so that they can be utilized in the backtest:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_market_data = pd.DataFrame(stock_data['Close'])\n",
    "stock_market_data = stock_market_data.rename(columns={'Close': 'PRICE'})\n",
    "stock_market_data = stock_market_data.set_index(pd.to_datetime(stock_data.index))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's inspect the top 5 rows of the prepared closing prices:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_market_data.head(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sub-sample the prepared daily closing prices to the out-of-sample time period:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_market_data = stock_market_data[stock_market_data.index >= signal_data.index[0]]\n",
    "stock_market_data = stock_market_data[stock_market_data.index <= signal_data.index[-1]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's inspect the top 5 rows of the prepared closing prices:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stock_market_data.head(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visualize the out-of-sample daily closing prices:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "ax.plot(stock_market_data['PRICE'], color='#9b59b6')\n",
    "\n",
    "for tick in ax.get_xticklabels():\n",
    "    tick.set_rotation(45)\n",
    "    \n",
    "# set axis labels\n",
    "ax.set_xlabel('[time]', fontsize=10)\n",
    "ax.set_ylabel('[equity %]', fontsize=10)\n",
    "\n",
    "for tick in ax.get_xticklabels():\n",
    "    tick.set_rotation(45)\n",
    "\n",
    "# set axis labels and limits\n",
    "ax.set_xlabel('[time]', fontsize=10)\n",
    "ax.set_xlim([stock_market_data.index[0], stock_market_data.index[-1]])\n",
    "ax.set_ylabel('[adj. closing price]', fontsize=10)\n",
    "\n",
    "# set plot title\n",
    "plt.title('International Business Machines Corporation (IBM) - Daily Historical Stock Closing Prices', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's calculate the potentially gained return by the application of a simple **\"buy and hold\"** strategy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.abs(stock_market_data.iloc[0]['PRICE'] - stock_market_data.iloc[-1]['PRICE']) / stock_market_data.iloc[0]['PRICE']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ok, with such a simple strategy we would have been able to yield a total return of approx. **5.32%**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 6.3. Backtest Preparation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have trading signals as well as the market data let's implement the LSTM based trading strategy which we name `LSTMStrategy`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class LSTMStrategy(bt.Algo):\n",
    "    \n",
    "    def __init__(self, signals):\n",
    "        \n",
    "        # set class signals\n",
    "        self.signals = signals\n",
    "        \n",
    "    def __call__(self, target):\n",
    "        \n",
    "        if target.now in self.signals.index[1:]:\n",
    "            \n",
    "            # get actual signal\n",
    "            signal = self.signals[target.now]\n",
    "            \n",
    "            # set target weights according to signal\n",
    "            target.temp['weights'] = dict(PRICE=signal)\n",
    "            \n",
    "        # return True since we want to move on to the next timestep\n",
    "        return True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's instantiate our LSTM based trading strategy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lstm_strategy = bt.Strategy('lstm', [bt.algos.SelectAll(), LSTMStrategy(signal_data['SIGNAL']), bt.algos.Rebalance()])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Initialize the backtest of our LSTM based trading strategy using the strategy and prepared market data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest_lstm = bt.Backtest(strategy=lstm_strategy, data=stock_market_data, name='stock_lstm_backtest')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In addition, let's also prepare a backtest of a \"baseline\" buy-and-hold trading strategy for comparison purposes. Our buy-and-hold strategy sends a \"long\" (+1.0) signal at each time step of the out-of-sample time frame: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "signal_data_base = signal_data.copy(deep=True) \n",
    "signal_data_base['SIGNAL'] = 1.0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Init the buy-and-hold (\"base\") strategy as well as the corresponding backtest:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "base_strategy = bt.Strategy('base', [bt.algos.SelectAll(), LSTMStrategy(signal_data_base['SIGNAL']), bt.algos.Rebalance()])\n",
    "backtest_base = bt.Backtest(strategy=base_strategy, data=stock_market_data, name='stock_base_backtest')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 6.4. Running the Backtest and Evaluate Results"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Run the backtest for both trading strategies: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest_results = bt.run(backtest_lstm, backtest_base)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the individual backtest results and performance measures:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest_results.display()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Collect detailed backtest performance per timestep of the LSTM trading strategy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest_lstm_details = backtest_lstm.strategy.prices.to_frame(name='Rel. EQUITY')\n",
    "backtest_lstm_details['Abs. EQUITY'] = backtest_lstm.strategy.values # equity per timestep\n",
    "backtest_lstm_details['CASH'] = backtest_lstm.strategy.cash # cash per timestep\n",
    "backtest_lstm_details['POSITIONS'] = backtest_lstm.strategy.positions # positions per timestep\n",
    "backtest_lstm_details['FEES'] = backtest_lstm.strategy.fees # trading fees per timestep"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the LSTM trading strategy backtest details:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest_lstm_details.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visualize the monthly returns obtained by the LSTM based trading strategy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "# plot heatmap of monthly returns generated by the strategy\n",
    "ax = sns.heatmap(backtest_lstm.stats.return_table, annot=True, cbar=True, vmin=-0.5, vmax=0.5)\n",
    "\n",
    "# set axis labels\n",
    "ax.set_xlabel('[month]', fontsize=10)\n",
    "ax.set_ylabel('[year]', fontsize=10)\n",
    "\n",
    "# set plot title\n",
    "ax.set_title('International Business Machines Corporation (IBM) - Monthly Returns LSTM Strategy', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Collect detailed backtest performance per timestep of the \"buy-and-hold\" trading strategy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest_base_details = backtest_base.strategy.prices.to_frame(name='Rel. EQUITY')\n",
    "backtest_base_details['Abs. EQUITY'] = backtest_base.strategy.values # equity per timestep\n",
    "backtest_base_details['CASH'] = backtest_base.strategy.cash # cash per timestep\n",
    "backtest_base_details['POSITIONS'] = backtest_base.strategy.positions # positions per timestep\n",
    "backtest_base_details['FEES'] = backtest_base.strategy.fees # trading fees per timestep"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inspect the \"buy-and-hold\" trading strategy backtest details:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest_base_details.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visualize the monthly returns obtained by the \"buy-and-hold\" trading strategy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "# plot heatmap of monthly returns generated by the strategy\n",
    "ax = sns.heatmap(backtest_base.stats.return_table, annot=True, cbar=True, vmin=-0.5, vmax=0.5)\n",
    "\n",
    "# set axis labels\n",
    "ax.set_xlabel('[month]', fontsize=10)\n",
    "ax.set_ylabel('[year]', fontsize=10)\n",
    "\n",
    "# set plot title\n",
    "ax.set_title('International Business Machines Corporation (IBM) - Monthly Returns \\'buy-and-hold\\' Strategy', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visualize the equity progression of both strategies over time:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = [15, 5]\n",
    "fig = plt.figure()\n",
    "ax = fig.add_subplot(111)\n",
    "\n",
    "ax.plot(backtest_lstm_details['Rel. EQUITY'], color='C1',lw=1.0, label='lstm strategy (green)')\n",
    "ax.plot(backtest_base_details['Rel. EQUITY'], color='C2',lw=1.0, label='base strategy (red)')\n",
    "\n",
    "for tick in ax.get_xticklabels():\n",
    "    tick.set_rotation(45)\n",
    "    \n",
    "# set axis labels\n",
    "ax.set_xlabel('[time]', fontsize=10)\n",
    "ax.set_xlim(valid_stock_sequence_data_date[:, -1].min(), valid_stock_sequence_data_date[:, -1].max())\n",
    "ax.set_ylabel('[equity %]', fontsize=10)\n",
    "\n",
    "# set plot legend\n",
    "plt.legend(loc=\"upper right\", numpoints=1, fancybox=True)\n",
    "\n",
    "# set plot title\n",
    "plt.title('International Business Machines Corporation (IBM) - Backtest % Equity Progression', fontsize=10);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Exercises:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We recommend you to try the following exercises as part of the lab:\n",
    "\n",
    "**1. Evaluation of Shallow vs. Deep RNN Models.**\n",
    "\n",
    "> Download the daily closing prices of the IBM stock within the time frame starting from 01/01/1990 until 05/31/2019. In addition to the architecture of the lab notebook, evaluate further (more shallow as well as more deep) RNN architectures by either: (1) re-moving/adding layers of LSTM cells, and/or (2) increasing/decreasing the dimensionality of the LSTM cells hidden state. Train your model (using architectures you selected) for at least 20'000 training epochs but keep the following parameters unchanged (a) sequence length: 5 time-steps (days) and (b) train vs. test fraction: 0.9.\n",
    "\n",
    "> Analyze the prediction performance of the trained models in terms of training time and prediction accuracy. Furthermore, backtest the out-of-sample signals predicted by each of your models and evaluate them in terms of total return and equity progression. Which of your architecture results in the best performing model, and why?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ***************************************************\n",
    "# INSERT YOUR CODE HERE\n",
    "# ***************************************************"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**2. Training and Evaluation of Models Learned from Additional Stocks.**\n",
    "\n",
    "> Download the daily closing prices of at least two additional stocks (e.g., Alphabet, Deutsche Bank) within the time frame starting from 01/01/1990 until 05/31/2017. Pls. select two stocks that you are interested in to investigate ( e.g. stocks that you may occasionally trade yourself). Learn an ’optimal’ RNN model of both stocks and backtest their corresponding trade signals by following the approach outlined in the lab notebook regarding the IBM stock. Pls. keep the train vs. test dataset fraction fixed to 0.9, all other parameters of the data preparation and model training can be changed.\n",
    "\n",
    "> Analyse the performance of the learned models in terms of their prediction accuracy as well as their out-of-sample backtest performance (e. g. the total return and equity progression). What architectures and corresponding training parameters result in the best performing models?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ***************************************************\n",
    "# INSERT YOUR CODE HERE\n",
    "# ***************************************************"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**3. Training and Evaluation of Models Learned from Augmented Data.**\n",
    "\n",
    "> In the prior exercises, we used the historical daily returns of a single stock to learn a model that can predict the stocks’ future closing price (log- return) movement. However, one of the advantages of NN’s lies in their capability to learn a model from multiple sources of input data.\n",
    "For each of the two stocks (’target stocks’) that you selected in exercise 2. learn an ’optimal’ RNN model using the daily returns as a target label. However, before training your models augment the training data of each stock by the return sequences of at least three additional stocks. The additional stocks, used for data augmentation, should exhibit a high correlation to the historical closing prices of the target stock price movement you aim to model.\n",
    "\n",
    "> Analyse the performance of the learned models in terms of their prediction accuracy as well as their out-of-sample backtest performance (e. g. the total return and equity progression). Do you observe an improvement of the trained model in terms of out-of-sample backtest performance comparison to exercise 1.?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ***************************************************\n",
    "# INSERT YOUR CODE HERE\n",
    "# ***************************************************"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Lab Summary:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this seventh lab, a step by step introduction into **design, implementation, training and evaluation** of a LSTM neural network based trading strategy is presented. \n",
    "\n",
    "The strategy trades a specific financial instrument based on its historical market prices. The degree of success of the implemented strategy is evaluated based in its backtest performance with particular focus on (1) the strategy's **total return** as well as (2) its **equity progression** over time. \n",
    "\n",
    "The code provided in this lab provides a blueprint for the development and testing of more complex trading strategies."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You may want to execute the content of your lab outside of the Jupyter notebook environment, e.g. on a compute node or a server. The cell below converts the lab notebook into a standalone and executable python script. Pls. note that to convert the notebook, you need to install Python's **nbconvert** library and its extensions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# installing the nbconvert library\n",
    "!pip3 install nbconvert\n",
    "!pip3 install jupyter_contrib_nbextensions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's now convert the Jupyter notebook into a plain Python script:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!jupyter nbconvert --to script cfds_lab_10.ipynb"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": false,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "324.67498779296875px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
